package main;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Point;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

import javax.imageio.ImageIO;
import javax.swing.JOptionPane;

/**
 * @author Caitlin Leksana
 * @period 8
 * @class software design
 *
 */

public class Note 
{
	private static final int LEDGER_LINE_LENGTH = Score.NOTE_HEAD_SIZE*2;
	private final int ANGLE = 15;
	public static final int SPACE_BETWEEN_NOTES = Score.NOTE_HEAD_SIZE+15;
	private static final BasicStroke OPEN_NOTE_STROKE = new BasicStroke(Score.NOTE_HEAD_SIZE/2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
	private static final String[] RESTS_INDEX = {"whole-rest.gif", "half-rest.gif", "quarter-rest.gif", "eighth-rest.gif", "sixteenth-rest.gif"};
	private static final char[] VALID_INPUTS = {'a','b','c','d','e','f','g','r'};

	private int accidentals;//1 = sharp, -1 = flat, 0 = natural
	private int octave;// positive = up octaves, negative = down octaves
	private String code;
	private char value;
	private double duration;
	private int position;//offset from 0 to where it is drawn
	private Score parent;
	private Area head;
	private Area stem;
	private Area note;
	private Area ledgerLines = new Area();
	private boolean stemUp = true;
	private boolean offStaff= false;
	private BufferedImage flag;
	private Point flagDrawPt;
	private boolean needFlag = false;
	private Color color = Color.BLACK;
	private boolean highlighted = false;
	private int horizontalTranslate;
	private boolean rest = false;
	private Image restImage = null;
	private boolean valid = false;
	private boolean empty = false;



	/**
	 * Creates a new note given a code.
	 * @param c code 
	 * @param s parent score
	 */
	public Note(String c, Score s)
	{
		code = c;
		parent = s;
		TimeSignature t = parent.getTimeSignature();
		duration = t.getOneBeat();
		octave = Score.BASE_OCTAVE;
		interpret();
		if(valid){
			if (rest) handleRest();
			else{
			position = Score.getPosition(this);
			generateSymbol();}
		}
	}

	/**
	 * Creates a new note
	 * @param v note name
	 * @param d duration
	 * @param p position
	 * @param a accidental (1 for sharp, -1 for flat)
	 * @param o octave
	 */
	public Note(char v, int d, int p, int a, int o)
	{
		value = v;
		duration = d;
		position = p;
		accidentals = a;
		octave = o;
	}

	/**
	 * Given the code of the note this determines and sets the value, octave and accidentals
	 */
	private void interpret()
	{
		char[] array = code.toCharArray();
		if(array.length == 0){
			empty = true;
			return;
		}
		value = array[0];
		for(int i = 0; i < VALID_INPUTS.length; i++){
			if(value == VALID_INPUTS[i]) valid = true;
		}
		if(!valid){
			return;
		}
		if (value == 'r') rest = true;
		for(int i = 1; i < array.length;i++)
		{
			if(array[i] > '0' && array[i] < '9')//number 
			{
				if(i < array.length - 1 && array[i+1] > '0' && array[i+1] < '9')//double number ex. 16
				{
					duration = Double.parseDouble(array[i]+ "")*10 + Double.parseDouble(array[i+1]+"");
					i++;
				}
				else//if single number ex. 4
				{
					duration = Double.parseDouble(array[i]+"");
				}
			}//close if
			if(rest) return;
			else if(array[i] == '+' || array[i] == '-' || array[i] == 'n')//sharp and flat
			{
				if(array[i] == '+')
					accidentals = 1;
				else if(array[i] == '-')
					accidentals = -1;
			}//close else if
			else//if ^, _, octave up or octave down
			{
				if(array[i] == '^'){
					octave++;
				}
				else if(array[i] == '_')
					octave--;
			}//close else
		}//close for
	}//close method


	/**
	 * Creates the graphical representation of the note
	 */
	private void generateSymbol(){
		int size = Score.NOTE_HEAD_SIZE;

		if(offStaff){ //Generates ledger lines if the note is off the staff
			int lines = Note.calculateLedgerLines(this);
			generateLedgerLines(lines);
			if (lines>0) position += 3;//general compensation offset
		}

		ArrayList<Integer> spacing = parent.getSpacingList();
		Ellipse2D.Double h = new Ellipse2D.Double(0,0,(int)(size*1.5), size);
		Point2D.Double center = new Point2D.Double(h.getCenterX(), h.getCenterY()); //Note head
		head = new Area(h);
		if(duration <= 2){
			int cutOut = 10;
			Ellipse2D.Double i = new Ellipse2D.Double(cutOut/2,cutOut*3/8,h.getWidth()-cutOut, h.getHeight()-(cutOut*3/4));
			Area inner = new Area(i);
			head.subtract(inner);
		}

		//rotates the head 
		AffineTransform moveToCenter = AffineTransform.getTranslateInstance(-center.x, -center.y);
		head.transform(moveToCenter);
		head.transform(AffineTransform.getRotateInstance(Math.toRadians(-ANGLE)));
		try {head.transform(moveToCenter.createInverse());} catch (NoninvertibleTransformException e) {}

		if(position <= spacing.get(spacing.size()/2)) //determines if the stem should go up or down
			stemUp = false;

		note = new Area(head);
		stem = new Area(new Rectangle2D.Double(1,5,Score.NOTE_STEM_WIDTH,Score.NOTE_STEM_LENGTH)); //stem of the note

		if (duration >= 8){ //adds flags if its an eighth note or faster
			needFlag = true;
			int flags = (int)(Math.log(duration)/Math.log(2))-2;
			addFlags(flags);
		}

		if(duration >= 2){
			note.add(new Area(stem)); //creates the note
		}

		if(stemUp){ //if the stem is up, it rotates the note and accounts for the change in horizontal offset
			note.transform(AffineTransform.getRotateInstance(Math.PI));
			position += Score.NOTE_HEAD_SIZE;
			note.transform(AffineTransform.getTranslateInstance(Score.NOTE_HEAD_SIZE, 0));
			if(needFlag) flagDrawPt.translate(Score.NOTE_HEAD_SIZE/2+2, -(Score.NOTE_STEM_LENGTH+Score.NOTE_HEAD_SIZE/2));
		}


	}

	/**
	 * If the note is an eighth note or faster it adds the appropriate nunmber of flags 
	 * @param flags number of flags
	 */
	private void addFlags(int flags) {
		Image flagImg = null;
		MediaTracker tracker = new MediaTracker(parent);
		try{
			flagImg = ImageIO.read(new File(stemUp? "flag_down.gif":"flag_up.gif"));
			tracker.addImage(flagImg, 0);
		}catch(IOException e){};
		tracker.checkAll();
		flagImg = flagImg.getScaledInstance(-1,Score.NOTE_STEM_LENGTH*3/4, Image.SCALE_SMOOTH);
		flag = new BufferedImage(flagImg.getWidth(null), Score.NOTE_STEM_LENGTH, BufferedImage.TYPE_INT_ARGB);
		Graphics2D flagGraphics = flag.createGraphics();
		int vOff = stemUp? 0: flag.getHeight()-flagImg.getHeight(null);
		for(int i = 0; i<flags; i++){
			flagGraphics.drawImage(flagImg,0, vOff, null);
			vOff += (stemUp? 1:-1)*flagImg.getHeight(null)/2;
		}
		flagDrawPt = new Point(Score.NOTE_STEM_WIDTH, 0);

	}

	/**
	 * Calculates the number of ledger lines that are needed to be drawn
	 * @param n given note
	 * @return number of ledger lines
	 */
	public static int calculateLedgerLines(Note n){
		int p = n.getPosition();
		ArrayList<Integer> spacing = n.getParent().getSpacingList();
		int bottom = spacing.get(spacing.size()-1);
		int top = spacing.get(0);
		int spaceBetween = Score.BAR_LINE_WIDTH + Score.BAR_LINE_SPACING;
		int diff = 0;
		int lines = 0;
		if(p > bottom) diff = bottom-p;
		if(p < top) diff = top-p;
		lines = (int) Math.round(diff/spaceBetween);
		return lines;

	}

	/**
	 * highlights a note in red
	 */
	public void highlight(){
		color = Color.RED;
		parent.redraw();
		highlighted  = true;
	}

	/**
	 * unhighlights a note
	 */
	public void unhighlight(){
		color = Color.BLACK;
		parent.redraw();
		highlighted = false;
	}

	/**
	 * Checks to see if the point is contained within the drawn note
	 * @param pt point to be checked
	 * @return if the point is within the note
	 */
	public boolean contains(Point2D pt){
		Point2D p = new Point2D.Double(pt.getX()-horizontalTranslate, pt.getY()-position);
		System.out.println(p);
		System.out.println(note.getBounds());
		return note.contains(p);
	}

	/**
	 * Calls up and edit note dialog
	 */
	public void edit(){
		String newCode = JOptionPane.showInputDialog(parent, "Current note: " + code, "Edit Note", JOptionPane.PLAIN_MESSAGE);
		code = newCode;
		interpret();
		parent.redraw();
	}

	/**
	 * Generates the ledger line area
	 */
	private void generateLedgerLines(int lines) {
		for (int i = 0; i < Math.abs(lines); i++){
			Area curr = new Area(new Rectangle2D.Double(0,i*(Score.BAR_LINE_SPACING),LEDGER_LINE_LENGTH, Score.BAR_LINE_WIDTH));
			ledgerLines.add(curr);
		}
		AffineTransform ledgerLineTrans;
		if(lines < 0){
			ledgerLineTrans = AffineTransform.getTranslateInstance(-ledgerLines.getBounds().getWidth()/2 + 3,Score.TOTAL_STAFF_SIZE+Score.BAR_LINE_SPACING);
		}
		else{
			ledgerLines.transform(AffineTransform.getRotateInstance(Math.PI));
			ledgerLineTrans = AffineTransform.getTranslateInstance(Score.NOTE_HEAD_SIZE+Score.NOTE_HEAD_SIZE/2+3,-Score.BAR_LINE_SPACING);
		}
		ledgerLines.transform(ledgerLineTrans);	
	}

	/**
	 * Draws the note
	 * @param g Graphics object
	 * @param hOff current cursor position
	 * @return new cursor position
	 */
	public int draw(Graphics2D g, int hOff){
		horizontalTranslate = hOff;
		g.setColor(color);
		g.translate(hOff, position);
		if(!rest){
			g.fill(note);
			if(duration >=8)
				g.drawImage(flag,flagDrawPt.x, flagDrawPt.y,null);
		}else{ 
			g.drawImage(restImage, 0, 0,null);
		}
		g.translate(0,-position);
		if(offStaff) g.fill(ledgerLines);
		g.translate(-hOff,0);
		hOff += stemUp?SPACE_BETWEEN_NOTES*1.25:SPACE_BETWEEN_NOTES;
		g.setColor(Color.BLACK);
		return hOff;
	}

	/**
	 * Allows the program to handle rests and draw them 
	 */
	private void handleRest() {
		int index = (int)(Math.log(duration)/Math.log(2));
		String fileName = RESTS_INDEX[index];
		try{
			restImage = ImageIO.read(new File(fileName));
		}catch(IOException e){}
		MediaTracker tracker = new MediaTracker(parent);
		tracker.addImage(restImage,0);
		tracker.checkAll();
		ArrayList<Integer> spacing = parent.getSpacingList();


		switch(index){
		case 0: restImage = restImage.getScaledInstance(-1, Score.BAR_LINE_WIDTH + Score.BAR_LINE_SPACING/2, Image.SCALE_SMOOTH);
				position = spacing.get(5);
				break;
		case 1: restImage = restImage.getScaledInstance(-1, Score.BAR_LINE_WIDTH + Score.BAR_LINE_SPACING/2, Image.SCALE_SMOOTH);
				position = spacing.get(4);
				break;
		case 2: restImage = restImage.getScaledInstance(-1, Score.BAR_LINE_WIDTH*3 + Score.BAR_LINE_SPACING*3, Image.SCALE_SMOOTH);
				position = spacing.get(2) ;
				break;
		case 3: restImage = restImage.getScaledInstance(-1, Score.BAR_LINE_WIDTH + Score.BAR_LINE_SPACING*2, Image.SCALE_SMOOTH);
				position = spacing.get(3) + Score.NOTE_HEAD_SIZE/4;
				break;
		case 4: restImage = restImage.getScaledInstance(-1, Score.BAR_LINE_WIDTH*2 + Score.BAR_LINE_SPACING*3, Image.SCALE_SMOOTH);
				position = spacing.get(3) + Score.NOTE_HEAD_SIZE/4;
				break;
		}
	}

	/**
	 * 
	 * @return Note name
	 */
	public char getValue()
	{
		return value;
	}

	/**
	 * 
	 * @return note duration
	 */
	public double getDuration()
	{
		return 1/duration;
	}

	/**
	 * 
	 * @return vertical offset
	 */
	public int getPosition()
	{
		return position;
	}

	/**
	 * Sets note value 
	 * @param v value to be set 
	 */
	public void setValue(char v)
	{
		value = v;
	}

	/**
	 * Sets duration
	 * @param d duration to be set
	 */
	public void setDuration(int d)
	{
		duration = d;
	}

	/**
	 * sets position
	 * @param p position to be set
	 */
	public void setPosition(int p)
	{
		position = p;
	}

	/**
	 * 
	 * @return octave note is in
	 */
	public int getOctave(){
		return octave;
	}

	/**
	 * 
	 * @return parent score
	 */
	public Score getParent(){
		return parent;
	}

	/**
	 * @return value(octave) Duration: d Position: p
	 */
	public String toString(){
		String s = value + "";
		switch(accidentals){
		case 1: s+="#";
		break;
		case -1: s+="b";
		break;
		}
		s+=octave;
		s+="\tDuration: ";
		s+="1/" + duration;
		s+="\t Position: " + position;
		return s;
	}
	public boolean isStemUp() {
		return stemUp;
	}

	public boolean isHighlighted(){
		return highlighted;
	}

	/**
	 * Sets whether or not the 
	 * @param t if the note is off the staff
	 */
	public void setOffStaff(boolean t){
		offStaff = t;
	}
	
	/**
	 * 
	 * @return if the note value is valid (a-g, r)
	 */
	public boolean isValid(){
		return valid;
	}
	
	public boolean isEmpty(){
		return empty;
	}




}//close Note class
